eBay支付核心账务系统之“展”翅高飞
作者|张志远
编辑|林颖
供稿|Payments Team
本文共6657字,预计阅读时间15分钟
更多干货请关注“eBay技术荟”公众号
导读
eBay[1]支付核心账务系统FAS[2](Financial Accounting System)后台仅包含一组PU(Processing Unit)集群,而单PU集群的处理能力具有上限。为保障eBay支付的业务发展不受限于FAS的处理能力,FAS团队未雨绸缪,开始了FAS架构的水平扩展之路。
水平扩展后的FAS不光在处理能力上实现线性提升,还可以做到扩展过程中无需停服并对业务系统实现完全透明。本文将讲述FAS是如何“展“翅高飞,以达到高容错、低风险和高性能的无缝扩展目标。
1 背景
图1 FAS架构
(点击可查看大图)
上游的业务逻辑层(Business Service)通过网关(Gateway)将转账请求发送给基于Raft共识算法的PU,PU安全更新状态机后返回转账结果(详见:超越‘双十一’|ebay支付核心账务系统架构演进之路)。这里的PU是有状态服务,而单PU集群处理能力具有上限,需要借助水平扩展来提升FAS的吞吐量。
在传统金融系统(如银行)中,对有状态服务进行水平扩展时往往需要停服数小时,待所有数据准备完毕并联调通过后才能继续提供线上服务;而在停服的过程中往往又需要上下游系统的配合,加重了系统扩展的复杂性。另外,相比于被称为分布式服务设计“银弹”的无状态服务,业界还没有针对有状态服务扩展的成熟方案。所以,如何对有状态服务进行扩展并实现高可用性是我们升级FAS架构的难点之一。
此外,在FAS中,PU的状态机中保存了处理过的全部账户余额信息。由于线上持续的流量输入,使得PU状态机总是处于被更新状态。由此,如何实现扩展过程不停服的同时保证状态机的正确性,也是FAS团队面临的巨大挑战。
在保证FAS水平扩展高可用性和正确性的同时,对扩展过程还有三大非功能性的要求:
1)系统高性能:扩展为N个PU集群后的最高负载,需要是单个PU集群最高负载的N倍;
2)扩展透明化:对现有业务逻辑不能有任何影响,不能要求现有业务逻辑做出改变以适配架构的升级;
3)操作方便性:在扩展过程中,要尽量减少对生产环境的手工配置或更改,以免引入潜在的风险。
针对上述需求,并结合之前FAS架构特点,FAS团队创造性地对传统Raft的实现进行改造,设计了一套严谨的高容错、低风险、高性能的弹性扩展方案,保障了FAS水平扩展的无缝进行。
2 方案设计
2.1 PU集群流量拆分
由于线上持续的转账流量,PU状态机总是处于被更新状态;而把部分流量切换到新PU集群,又需要实现PU状态机的复制。如何在不影响线上流量的同时实现状态机的复制,是首先要解决的问题。
在FAS中,状态机的每一次余额更新均会以事件(Event)的形式记录在日志(Raft Log)中。只要把源PU集群的所有日志完整地复制并应用到新PU集群状态机,就可以达到复制状态机的目的。借鉴FAS”读模块”[3]设计思路,新PU集群的节点可以作为顶层节点(L0)从源PU集群高效地拉取日志(Golden Source)。
鉴于FAS自上线以来产生了大量的日志文件,若完整应用全部日志,则会将耗费大量的时间。在实际操作中,新PU集群并没有拉取全部的日志。新PU集群不是从零构建,而是基于源PU最近的快照(Snapshot),扩展时仅需应用从快照到日志边界(即扩展命令)之间的所有日志(δEvents),这节省了网络带宽和时间成本,用公式表示如下:
2.1.2 Raft改造
有了源PU集群状态机复制的方案后,我们还需要考虑日志污染问题。新PU集群在处理线上流量之前,它的日志全部来自于源PU集群,而每一条转账请求对应的日志索引(Log Index)在转账事实发生时已经确定。如果新PU集群以Raft模式启动,就会因Raft选举产生的新Leader发送Noop命令,而造成日志污染。
为避免日志污染,FAS团队创造性地改造了传统的Raft实现。PU集群节点在启动时默认不开启Raft模块,只有在满足下面任意一个条件时才会工作在Raft模式:
1)自身状态机中有所属PU集群分组流量路由配置;
2)远端的全局路由表中有所属PU集群分组流量路由配置;
3)明确收到启动Raft指令。
前2点用于保证在日常流量处理时,任意PU集群节点宕机重启后,可自动加入Raft集群处理线上请求;第3点保证FAS扩展过程将处于严格的控制之下。
2.1.3 流量拆分
Raft实现改造后,新PU集群的节点作为”读模块”可以从源PU集群拉取日志,这就需要确定拉取的边界,即何时完成日志拉取并将流量从源PU集群拆分到新PU集群。
为确保状态机的正确性,新PU集群节点完成日志拉取的前提条件是:源PU集群停止处理迁移出来的分组流量。这就需要一种机制——在源PU集群完成流量拆分后,通知新PU集群节点:“日志拉取的边界就是源PU集群进行流量拆分时刻日志所在的索引”。
另外一个问题是,源PU集群的节点必须保持对流量拆分事件的共识。如果没有达成共识,在流量拆分后,就会因Raft选举换主而造成新的Leader节点继续处理已经拆分出去的分组流量。而为了达成流量拆分共识,源PU集群Leader节点需要将扩展指令(tag event)写入日志。该日志索引之后的所有日志均不包含已经拆分出去的分组流量转账记录,也就自然成了新PU集群节点的日志拉取边界。
当新PU集群中足够多的节点顺利拉取到扩展指令日志(tag event)后,就可以向其发送启动Raft模式指令,并在Raft模式启动成功后开始处理拆分出来的分组流量。整个过程如图2所示:
图2 流量拆分
(点击可查看大图)
日志拉取边界的确定,可以保证PU状态机的正确性,但分组流量从源PU集群拆分到新PU集群处理之间存在时间间隔,这段间隔如果很长就会导致线上请求超时失败。
日志记录的是已经发生的转账事实。新PU集群节点在应用日志到状态机时无需进行去重等校验,也无需等待集群中多数派节点成功备份。单纯的日志拉取和应用的性能是处理线上转账请求性能的数十倍以上。基于此,新PU集群节点和源PU集群日志之间的“距离”最终会近似收敛于节点间日志传输的网络延时,即做到”准实时”同步。只要在新PU集群节点和源PU集群日志”准实时”同步后发送扩展命令,就可以保证在很短时间内完成PU集群间的流量拆分。
2.2 业务系统流量切换
网关承担着路由的重要职责,在下游PU集群实现流量拆分后,需要有一种机制来通知后知后觉的网关来更新其路由配置。整个流量切换过程网关视角如图3所示:
图3 流量切换
(点击可查看大图)
1)网关把带有分组信息的转账请求发送给源PU集群(PU cluster #0);
2)源PU集群未收到扩展命令前,正常处理转账请求;收到扩展命令后,更新状态机里的路由表,并判断随后的分组流量是否在自己的路由表中,如果不属于自己的路由范围,拒绝该请求并返回路由错误;
3)网关收到路由错误后,从远端存储查询最新全局路由配置,并根据最新路由配置将流量转发到正确的PU集群(PU cluster #1);如果全局路由配置还没有更新,则转至第1步。
在网关查询最新全局路由配置的同时,新PU集群会在很短时间内运行在Raft模式,等待处理网关路由过来的分组流量。整个扩展过程隐藏在网关的路由层,无需更改上游业务系统,并对业务系统透明。受益于日志的”准实时”同步,整个路由切换在毫秒级时间内完成。
2.3 协调控制
图4 方案设计
(点击可查看大图)
整个扩展过程描述如下:
1)管理员向Scale Controller发送流量切换请求;
2)Scale Controller收到流量切换请求后,并行通知新搭建PU集群(以非Raft模式启动)中的所有节点从源PU集群复制日志;
3)收到日志拉取命令后,新PU集群中的节点开始从源PU集群拉取日志;
4)Scale Controller不停地查询新PU集群节点日志的拉取状态,当所有节点拉取到的日志和源PU集群日志足够接近同步(即准实时)时,给源PU集群发送扩展命令;
5)源PU集群收到扩展命令后,在日志(Raft Log)里记录该扩展命令(Tag Event),并更新状态机里的路由表;
6)源PU集群向Scale Controller返回扩展成功;
7)当Scale Controller查询到新PU集群中有足够多的节点已经顺利拉取到步骤5中写入的扩展命令(tag event)后,向其发送启动Raft模式指令;
8)Scale Controller更新全局路由表到远端存储。
Scale Controller的设计使整个水平扩展过程自动化,管理员只需要向其发送一条扩展命令,这样成功地满足了易操作的需求。但想要增强扩展过程的弹性却并非易事,需要考虑存储选型、Scale Controller单点故障等一系列问题。
2.3.1 存储选型
FAS系统的全局路由信息需要存储在远端存储中。常用的存储解决方案通常是数据库或基于键值(Key-value)存储的Redis[4]。它们都有主从备份,但为了提高性能,一般备份是异步进行的。在极端情况下,主库宕机时会丢失从库还没来得及备份的数据。
全局路由是十分重要的配置信息,FAS团队采用同样基于Raft共识算法的ETCD[5]作为远端存储。与PU集群一样,ETCD部署在分布于3个数据中心的5台机器上,保证了存储的高可靠性。此外,ETCD支持的事务、租约(Lease)、分布式锁等机制也给Scale Controller单点故障的消除提供了重要支持。
2.3.2 单点故障
Scale Controller是单点,一旦在流量切换过程中宕机会造成严重的后果,因此必须实现高可用。首先,Scale Controller需要有多个节点,跟PU集群部署方式类似,我们把Scale Controller部署在3个数据中心的多台服务器上;其次,Scale Controller需要支持分布式任务调度功能,当处理扩展任务的节点发生宕机时,其它节点需要在很短时间内感知到,再领取任务并继续执行。为此,FAS团队研发了一款轻量级分布式任务调度框架,取名不倒翁(Tumbler)。
Tumbler使用ETCD作为后端存储,支持至少一次(at-least-once)语义,保证任务至少会被成功执行一次。适配了Tumbler框架的Scale Controller调度如图5所示:
图5 基于Tumbler的Scale Controller
(点击可查看大图)
1)初始时Scale Controller所有节点均是对等的Worker节点,其中任意的Worker节点接收到扩展请求后新建一个扩展任务,并将该任务存储到ETCD;
2)Scale Controller中的所有Worker节点定期查询ETCD中是否有还未被领取的任务;如果有还未被领取的任务,会通过ETCD分布式锁选出一个Executor节点来领取该任务;
3)Executor节点领取任务后开始执行,并保持与ETCD之间的心跳来维持Executor身份;
4)Executor节点顺利完成任务后,从ETCD中删除任务;如果在执行任务过程中Executor节点宕机,心跳结束,则转至步骤2。
2.3.3 表驱动模型
有了Tumbler框架,Scale Controller单点故障问题得以解决。但每次Scale Controller Executor节点宕机,整个扩展过程都需要从头重新执行。如果Executor节点宕机前已经向源PU集群发送完扩展命令,这将导致流量切换的停顿时间变长。为了最大限度地降低路由切换的停顿间隔,FAS团队将Scale Controller的扩展任务设计为由一系列基于表驱动[6]的状态组成。每个状态都对应有自己的动作(Action),当前状态的动作完成后,切换到新的状态,并将最新状态更新到ETCD。Executor节点宕机后,新Executor节点从ETCD获取当前任务的最新状态,随后执行该状态对应的动作。简略的状态迁移如图6所示:
图6 Scale Controller状态迁移图
(点击可查看大图)
1)任意状态出错都会进入Error状态;
2)所有状态都带有超时模式,超时后进入Error状态;
3)所有状态的动作都遵循快速失败(Fast-fail[7])机制,在发送扩展命令前不允许任何新PU集群节点宕机;发送扩展命令后最多允许1台新PU集群节点宕机;
4)在Error状态动作里,会判断是否已发送扩展命令,若是则执行扩展回滚。
2.3.4 容错机制
Tumbler框架利用与ETCD之间的心跳解决单点故障,但心跳不能百分百保证当前只有一个Executor节点。有可能老的Executor节点因为网络抖动等原因导致心跳发送失败,造成线上多个Executor节点的存在,使得扩展任务被执行多次。
由于心跳不稳定的问题很难修复,FAS团队在设计扩展方案时,决定选择除修复/规避问题的另一思路——拥抱这个问题。因此,接口设计需遵循以下原则:
1)PU与Scale Controller交互的接口中所有涉及状态更改的操作都要做去重处理,每个重复请求的处理结果都是“做完(done)”或“正在做(doing)”,以保证系统状态中不会存在”薛定谔的猫”[8];
2)所有Scale Controller更新ETCD中键值的操作都需要使用事务,借助CAS(Compare-And-Swap)[9]来实现仅第一个更新操作才能成功;如果更新事务失败,说明系统中有其它Executor节点存在,并且其它Executor节点执行比自身快,需要忽略自身领取的任务并退出Executor身份。
2.3.5 快慢节点机制
有了容错机制,可以保证FAS扩展时不会出错,流量的快速切换成了FAS团队新的追求。从源PU集群响应扩展命令到Scale Controller更新全局路由表的这段时间内,迁移出的账号分组流量处于无集群处理状态;这段时间间隔的尽量减少将大大减轻网关的压力。
由于每个数据中心网络环境不同等因素,不同PU节点的响应速度有差别。Raft共识算法要求多数派达成一致,即可组成集群来响应线上请求。因此,可以将节点分为快节点和慢节点两类,优先完成日志拉取的成为快节点。只要快节点数量达到最小多数派(5个节点中的3个),即可发送启动Raft模式指令,然后更新全局路由表。剩下的慢速节点,可以在全局路由表更新结束后,再等待其完成日志拉取并加入Raft集群。
在实际操作中,为避免最小多数派节点中任一节点宕机导致的集群不可用,FAS团队将快节点数量定义为最小多数派加1,即集群5个节点中有4个节点成功拉取日志后就开始后续操作,这保证了正确性的同时,还兼顾了高性能。
3 故障演练
分布式系统大多构建在大规模廉价的机器之上,相较于传统的单机软件系统,更容易发生各种非预期的异常。在深入分析FAS水平扩展解决方案的各种风险点后,FAS团队通过组合、正交等方式设计了一整套故障演练灾难场景,如下表所示。
表1 灾难场景汇总
根据这些灾难场景,FAS团队在故障演练过程中,通过手工杀进程、程序模拟网络或节点随机故障、引入Chaos Monkey[10]等方式进行各类灾难场景重现。
图7为演练中出现的Scale Controller 3次连续宕机;图8从左向右分别为新PU集群Raft模式启动失败、日志拉取出现故障和发送扩展命令失败演练。
图7 连续宕机3次的Scale Controller
(点击可查看大图)
图8 部分故障演练示意
(点击可查看大图)
经过十余轮故障演练,FAS水平扩展方案在迭代优化中具备了很强容错性,最终确保了所有灾难演练场景均得到预期结果:
1)安全失败:在发送扩展命令前出错,扩展失败,对线上系统无影响;
2)成功:保证了水平扩展的正确性,同时在毫秒级别实现流量切换;
3)回滚:发送完扩展命令后出错,但流量切换在很短时间内回滚,对线上系统无影响。
4 总结
在实际上线过程中,仅花费几分钟的日志拉取(过程如图9所示),而流量切换更是在1秒内完成(如图10所示)。
图9 日志拉取
(点击可查看大图)
图10 扩展任务
(点击可查看大图)
通过压力测试对比(如图11、12所示),扩展为双PU集群的FAS总TPS约为扩展前单PU集群的两倍。
图11 扩展前TPS压测
(点击可查看大图)
图12 扩展后TPS压测
(点击可查看大图)
通过上述对FAS水平扩展方案的介绍,我们希望能为对数据处理有同等需求(高可靠和高性能)的行业(如金融、医疗等),在架构升级方面提供一些参考。
5 展望
未来,FAS团队还将支持PU集群流量的合并,融合了扩展和合并功能的FAS将具有更高维度的动态可伸缩性。
参考资料
[1]https://www.ebay.com/
[2]eBay支付核心账务系统架构演进之路:
https://mp.weixin.qq.com/s/O5_Rde5uUXvmBS2B7w2hOQ
[3]eBay支付账务系统架构解析之“读”一无二:
https://mp.weixin.qq.com/s/SghlRwIOdMzUlqnb2QwdA
[4]https://redis.io/
[5]https://etcd.io/
[6]https://www.techopedia.com/definition/30408/table-driven-design
[7]https://en.wikipedia.org/wiki/Fail-fast
[8]https://en.wikipedia.org/wiki/Schr%C3%B6dinger%27s_cat
[9]https://en.wikipedia.org/wiki/Compare-and-swap
[10]https://netflix.github.io/chaosmonkey/
往期推荐
干货|eBay基于Istio的应用网关的探索和实践
eBay支付账务系统架构解析之“读”一无二
ClickHouse集群|Operator跨k8s集群管理
超越“双十一”|eBay支付核心账务系统架构演进之路
超越“双十一”|eBay百万TPS支付账务系统设计与实现
点击阅读原文,一键投递